package core;

import java.util.Iterator;
import java.util.ArrayList;
import stat.BallStat;

import static com.googlecode.javacv.cpp.opencv_core.IplImage;
import static com.googlecode.javacv.cpp.opencv_core.CvMemStorage;
import static com.googlecode.javacv.cpp.opencv_core.CvPoint;
import static com.googlecode.javacv.cpp.opencv_core.CvPoint2D32f;
import static com.googlecode.javacv.cpp.opencv_core.CvPoint3D32f;
import static com.googlecode.javacv.cpp.opencv_core.CvSeq;
import static com.googlecode.javacv.cpp.opencv_core.CvScalar;
import static com.googlecode.javacv.cpp.opencv_core.CvContour;
import static com.googlecode.javacv.cpp.opencv_core.CV_RGB;
import static com.googlecode.javacv.cpp.opencv_core.CV_FILLED;
import static com.googlecode.javacv.cpp.opencv_core.CV_WHOLE_SEQ;
import static com.googlecode.javacv.cpp.opencv_core.IPL_DEPTH_8U;
import static com.googlecode.javacv.cpp.opencv_core.cvCircle;
import static com.googlecode.javacv.cpp.opencv_core.cvCreateImage;
import static com.googlecode.javacv.cpp.opencv_core.cvCreateMemStorage;
import static com.googlecode.javacv.cpp.opencv_core.cvCreateSeq;
import static com.googlecode.javacv.cpp.opencv_core.cvGetSeqElem;
import static com.googlecode.javacv.cpp.opencv_core.cvSeqPush;
import static com.googlecode.javacv.cpp.opencv_core.cvGetSize;
import static com.googlecode.javacv.cpp.opencv_core.cvLine;
import static com.googlecode.javacv.cpp.opencv_core.cvPoint;
import static com.googlecode.javacv.cpp.opencv_core.cvPointFrom32f;
import static com.googlecode.javacv.cpp.opencv_core.cvSize;
import static com.googlecode.javacv.cpp.opencv_core.cvDrawContours;
import static com.googlecode.javacv.cpp.opencv_core.cvSetZero;
import static com.googlecode.javacv.cpp.opencv_core.CvFont;
import static com.googlecode.javacv.cpp.opencv_core.CV_FONT_HERSHEY_PLAIN;
import static com.googlecode.javacv.cpp.opencv_core.cvPutText;
import static com.googlecode.javacv.cpp.opencv_core.cvClearMemStorage;

import static com.googlecode.javacv.cpp.opencv_imgproc.cvCanny;
import static com.googlecode.javacv.cpp.opencv_imgproc.cvCvtColor;
import static com.googlecode.javacv.cpp.opencv_imgproc.cvHoughCircles;
import static com.googlecode.javacv.cpp.opencv_imgproc.cvFindContours;
import static com.googlecode.javacv.cpp.opencv_imgproc.cvContourArea;
import static com.googlecode.javacv.cpp.opencv_imgproc.cvSmooth;
import static com.googlecode.javacv.cpp.opencv_imgproc.cvThreshold;
import static com.googlecode.javacv.cpp.opencv_imgproc.cvMinEnclosingCircle;
import static com.googlecode.javacv.cpp.opencv_imgproc.CV_RETR_EXTERNAL;
import static com.googlecode.javacv.cpp.opencv_imgproc.CV_CHAIN_APPROX_NONE;
import static com.googlecode.javacv.cpp.opencv_imgproc.CV_BGR2GRAY;
import static com.googlecode.javacv.cpp.opencv_imgproc.CV_GAUSSIAN;
import static com.googlecode.javacv.cpp.opencv_imgproc.CV_HOUGH_GRADIENT;
import static com.googlecode.javacv.cpp.opencv_imgproc.CV_THRESH_BINARY;
import com.googlecode.javacpp.Loader;
import com.googlecode.javacv.CanvasFrame;

/**
 * 
 * @author Hyunchang, Sangyoun, Yunseong Ball Detector from X-ray image of BGA
 *         (Ball-Grid Array)
 * 
 */
public class BallDetector {
	private IplImage original;
	private IplImage binary;
	private IplImage segmented;
	private IplImage cannyed;
	private int[] histogram;
	private int threshold;
	private CvSeq contours;
	private CvSeq houghBalls;
	ArrayList<Ball> filteredContours;

	private ArrayList<Ball> ballList;
	private ArrayList<BallStat> ballStat;
	private static final double CHECK_RADIUS_RATIO = 0.8;
	private static final double CHECK_DIAMETER_DIFF_RATIO = 0.3;


	// Constructor
	BallDetector(IplImage src) {
		// Store the original image.
		this.original = src.clone();

		// Convert the loaded image into a gray-scale image.
		IplImage binary = cvCreateImage(cvGetSize(src), src.depth(), 1);
		cvCvtColor(src, binary, CV_BGR2GRAY);
		this.binary = binary;

		this.segmented = null;
		this.histogram = null;
		this.contours = null;
		this.houghBalls = null;

		this.ballList = new ArrayList<Ball>();
		this.ballStat = new ArrayList<BallStat>();
		this.filteredContours = new ArrayList<Ball>();

	}

	// Get the balls detected
	public CvSeq getDetectedBalls() {
		return this.contours;
	}

	public ArrayList<Ball> getBallList() {
		return ballList;
	}
	public ArrayList<CvSeq> getBallsAL() {
		ArrayList<CvSeq> b = new ArrayList<CvSeq>();
		for (int i = 0 ; i < getBallList().size(); i++)
			b.add(this.getBallList().get(i).getBallContour());
		return b;
	}

	public ArrayList<BallStat> getBallStat() {
		return ballStat;
	}

	// Initialize a Histogram with the loaded image.
	private void initHistogram() {
		int height = this.binary.height();
		int width = this.binary.widthStep();

		int[] hist = new int[256];
		for (int i = 0; i < height; i++) {
			for (int j = 0; j < width; j++) {
				byte b = this.binary.imageData().get(i * width + j);
				if (b >= 0) { // // no zero
					hist[b]++;
				} else if (b < 0) { // && (int)b > -128)/////
					hist[256 + b]++;
				}
			}
		}
		this.histogram = hist;
	}

	// Adjust Mean value recursively to obtain threshold value.
	private static int adjustMean(int[] histogram, int mean) {
		int temp1 = 0;
		int temp2 = 0;
		int lessMean;
		int moreMean;

		for (int i = 0; i < mean; i++) {
			temp1 += histogram[i];
			temp2 += i * histogram[i];
		}
		lessMean = temp2 / temp1;

		for (int i = mean; i < 256; i++) {
			temp1 += histogram[i];
			temp2 += i * histogram[i];
		}
		moreMean = temp2 / temp1;

		int tempmean = (lessMean + moreMean) / 2;

		if (tempmean == mean)
			return mean;
		else
			return adjustMean(histogram, tempmean);
	}

	// Get Threshold value by Histogram Analysis.
	private void getThreshold() {
		int mean = adjustMean(this.histogram, 128);
		this.threshold = mean;
	}

	// Print the histogram.
	public void printHist() {
		CanvasFrame histFrame = new CanvasFrame("histogram");
		IplImage can = cvCreateImage(cvSize(256, 256), IPL_DEPTH_8U, 3);

		int max = 256 * 200;
		double scale = max > can.height() ? (double) can.height() / max : 1.;

		for (int i = 0; i < 256; i++) {
			CvPoint pt1 = cvPoint(i,
					(int) (can.height() - (this.histogram[i] * scale)));
			CvPoint pt2 = cvPoint(i, can.height());
			cvLine(can, pt1, pt2, CV_RGB(255, 255, 255), 1, 8, 0);
		}
		CvPoint pt1 = cvPoint(this.threshold, 0);
		CvPoint pt2 = cvPoint(this.threshold, can.height());
		cvLine(can, pt1, pt2, CV_RGB(255, 0, 0), 1, 8, 0);
		histFrame.showImage(can);
	}

	// Thresholding image with the threshold value obtained.
	public IplImage thresholding() {
		this.initHistogram();
		this.getThreshold();
		IplImage result = cvCreateImage(cvGetSize(this.binary),
				this.binary.depth(), 1);
		cvThreshold(binary, result, this.threshold, 255, CV_THRESH_BINARY);
		this.segmented = result;
		return result;
	}

	public IplImage cannying() {
		IplImage result = cvCreateImage(cvGetSize(this.segmented),
				this.segmented.depth(), 1);
		cvSmooth(this.segmented, result, CV_GAUSSIAN, 9, 9, 2.0, 0.0);
		//cvAdaptiveThreshold(result, result, 255, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, 151,-5);
		cvCanny(result, result, 50, 200, 3);
		this.cannyed = result;
		return result;
	}

	public IplImage houghCircles() {
		CvSeq circles = new CvSeq();
		IplImage result = this.original.clone();

		CvMemStorage mem = cvCreateMemStorage(0);

		// param1 is used for the (internally called) canny edge detector.
		// The first parameter of the canny is set to param1, and the second is set to param1/2. 
		// param2 sets the minimum number oat an accumulator cell needs to qualify as a possible circle. 
		double dp = 1;
		// Distribute point. Regular if the value is 1. Must be >= 1 
		double minDist = 30;
		// Circle if the value is near 1 and ellipse if the value is near 0
		double param1 = 20.0; 
		// Threshold for canny method. 
		double param2 = 30.0;
		// 2nd threshold for canny method
		int minRadius = 0; int maxRadius = 200;

		circles = cvHoughCircles(this.cannyed, mem, CV_HOUGH_GRADIENT, dp, minDist, param1, param2, minRadius, maxRadius);
		this.houghBalls = circles;

		for (int i = 0; i < circles.total(); i++) { // round the floats to an int
			CvPoint3D32f point = new CvPoint3D32f(cvGetSeqElem(circles, i));

			CvPoint center = cvPointFrom32f(new CvPoint2D32f(point.x(), point.y()));
			int radius = Math.round(point.z());

			// draw the circle center
			cvCircle(result, center, 3, CV_RGB(0,0,255), -1, 8, 0 );
			// draw the circle outline 
			cvCircle(result, center, radius+1, CV_RGB(255,0,0), 2, 8, 0 );
		}
		cvClearMemStorage(mem);
		return result;
	}

	/*
	 * Improved version of houghCircles()
	 */
	public IplImage houghCirclesFilter() {
		IplImage result = this.original.clone();

		CvMemStorage memo = cvCreateMemStorage(0);
		CvSeq candidates = cvCreateSeq(0, Loader.sizeof(CvSeq.class), Loader.sizeof(CvSeq.class), memo);

		// Detect all balls first
		for (int i = 0; i < this.houghBalls.total(); i++) {
			// round the floats to an int
			CvPoint3D32f point = new CvPoint3D32f(cvGetSeqElem(this.houghBalls, i));
			CvPoint center = cvPointFrom32f(new CvPoint2D32f(point.x(), point.y()));
			int radius = Math.round(point.z());

			// Choose 9 coordinates inside each detected ball
			CvPoint[] coordinates = new CvPoint[9];
			float x = point.x();
			float y = point.y();

			coordinates[0] = cvPointFrom32f(new CvPoint2D32f(x, y));
			coordinates[1] = cvPointFrom32f(new CvPoint2D32f(x, y + (radius * CHECK_RADIUS_RATIO)));
			coordinates[2] = cvPointFrom32f(new CvPoint2D32f(x - (radius * CHECK_RADIUS_RATIO), y));
			coordinates[3] = cvPointFrom32f(new CvPoint2D32f(x + (radius * CHECK_RADIUS_RATIO), y));
			coordinates[4] = cvPointFrom32f(new CvPoint2D32f(x, y - (radius * CHECK_RADIUS_RATIO)));
			coordinates[5] = cvPointFrom32f(new CvPoint2D32f(x - (radius * CHECK_RADIUS_RATIO *2/4), y + (radius * CHECK_RADIUS_RATIO *2/4)));
			coordinates[6] = cvPointFrom32f(new CvPoint2D32f(x + (radius * CHECK_RADIUS_RATIO *2/4), y + (radius * CHECK_RADIUS_RATIO *2/4)));
			coordinates[7] = cvPointFrom32f(new CvPoint2D32f(x - (radius * CHECK_RADIUS_RATIO *2/4), y - (radius * CHECK_RADIUS_RATIO *2/4)));
			coordinates[8] = cvPointFrom32f(new CvPoint2D32f(x + (radius * CHECK_RADIUS_RATIO *2/4), y - (radius * CHECK_RADIUS_RATIO *2/4)));

			int blackCount = 0;
			int insideCount = 0;
			
			for (int j = 0; j < 9; j++) {
				byte b = getPixel(coordinates[j].x(), coordinates[j].y(), this.segmented); 
				
				if(b == 0)
					blackCount++;
				
			}
			
			boolean doNotAdd = (blackCount < 8);
			for (int k = 0; k < this.filteredContours.size(); k++) {
				Ball ball = this.filteredContours.get(k);
			
				for (int l = 0; l < 9; l++) {
					if (ball.isInside(coordinates[l].x(), coordinates[l].y())) {
						insideCount++;
					}					
				}
				if (insideCount > 7) {
					doNotAdd = true;
					break;
				}
				insideCount = 0;
			}
			
			if (!doNotAdd) {
				cvSeqPush(candidates, cvGetSeqElem(this.houghBalls, i));
				cvCircle(result, center, radius + 1, CV_RGB(255, 0, 0), 1, 8, 0);
			}
		}
		this.houghBalls = candidates;
		cvClearMemStorage(memo);

		return result;
	}

	public IplImage findContours() {
		CvSeq contours = new CvSeq();
		IplImage result = this.original.clone();

		CvMemStorage mem = cvCreateMemStorage(0);

		cvFindContours(this.cannyed, mem, contours, Loader.sizeof(CvContour.class), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
		this.contours = contours;

		// Print the obtained contours
		for (CvSeq ptr = contours; ptr != null; ptr = ptr.h_next()) {
			cvDrawContours(result, ptr, CV_RGB(0,255,0), CV_RGB(0, 0, 0), -1, CV_FILLED,
					8, cvPoint(0, 0));
		}
		cvClearMemStorage(mem);
		return result;
	}

	// Filter the contours with the characteristic of circle-shaped.
	public IplImage filterContoursByCompactness() {
		IplImage result = this.original.clone();

		float[] radius = new float[128];

		for (CvSeq ptr = this.contours; ptr != null; ptr = ptr.h_next()) {
			CvPoint2D32f point = new CvPoint2D32f();
			cvMinEnclosingCircle(ptr, point, radius);
			double square = cvContourArea(ptr, CV_WHOLE_SEQ, 0);

			double ratio = (radius[0] * radius[0] * Math.PI / square);
			if (ratio < 1.4) {
				filteredContours.add(new Ball(ptr));
				cvDrawContours(result, ptr, CV_RGB(255, 0, 0), CV_RGB(0, 0, 0), -1, 1, 8, cvPoint(0, 0));
			}
		}
		return result;
	}

	public void filterContoursByPixel() {
		System.out.println("filterPixel" + filteredContours.size());
		ArrayList<Ball> temp = new ArrayList<Ball>();
		for ( int i = 0; i < filteredContours.size(); i++ ) {
			int centerX = filteredContours.get(i).getCenterX();
			int centerY = filteredContours.get(i).getCenterY();
			if (getPixel(centerX, centerY, this.segmented) == 0) {
				temp.add(filteredContours.get(i));
			}
		}
		System.out.println("filterPixel a" + temp.size());
		this.filteredContours = temp;		

	}

	public IplImage mergeCirclesAndContours()
	{
		IplImage result = original.clone();
		// getting contours from circles.
		// to get them we should create a blank image
		IplImage tempImage = cvCreateImage(cvGetSize(segmented), segmented.depth(), 1);
		cvSetZero(tempImage);

		CvMemStorage mem = cvCreateMemStorage(0);
		CvSeq contoursToMerge = new CvSeq();

		for (int i = 0; i < this.houghBalls.total(); i++) 
		{
			CvPoint3D32f point = new CvPoint3D32f(cvGetSeqElem(this.houghBalls, i));
			CvPoint center = cvPointFrom32f(new CvPoint2D32f(point.x(), point.y()));
			int radius2 = Math.round(point.z());

			cvCircle(tempImage, center, radius2, CV_RGB(255, 255, 255), 1, 8, 0);
		}

		// find contours from the image with circles
		cvFindContours(tempImage, mem, contoursToMerge, Loader.sizeof(CvContour.class), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);

		for (CvSeq ptr2 = contoursToMerge; ptr2 != null; ptr2 = ptr2.h_next()) {
			if(ptr2.isNull()) break;
			filteredContours.add(new Ball(ptr2));
			cvDrawContours(result, ptr2, CV_RGB(255, 0, 0), CV_RGB(0, 0, 0), -1, CV_FILLED, 8, cvPoint(0, 0));
		}
		return result;
	}

	// Draw Image over result with ArrayList ballsAL
	public IplImage writeNumbers(IplImage toWrite) {		
		Iterator<Ball> iter = this.ballList.iterator();
		int count=1;
		while (iter.hasNext()) 
		{
			Ball ball = iter.next();
			String num = Integer.toString(count);

			CvFont font = new CvFont(CV_FONT_HERSHEY_PLAIN, 1.2, 1);
			cvPutText(toWrite, num, new CvPoint(ball.getLeft(),ball.getBottom()), font, CvScalar.RED);
			count++;
		}

		// TODO Auto-generated method stub
		return toWrite;
	}

	public byte getPixel (int x, int y, IplImage toRead) {
		return toRead.imageData().get(y * toRead.widthStep() + x);
	}


	public void filterContoursByDiameter() {
		double averageDiameter = 0;
		int count = 0;

		Iterator<Ball> iter = filteredContours.iterator();
		while (iter.hasNext()) 
		{
			Ball ball = iter.next();
			double diameter =  ball.getRadius() * 2;

			averageDiameter += diameter ;
			count++;
		}

		// TODO Error preventing
		averageDiameter = (count == 0)? 0 : averageDiameter / count;

		ArrayList<Ball> temp = new ArrayList<Ball>();
		for( int i = 0; i < filteredContours.size(); i++ ) {
			double diameter = filteredContours.get(i).getRadius() * 2 ; 
			if (diameter > averageDiameter*(1-CHECK_DIAMETER_DIFF_RATIO) && diameter < averageDiameter * (1+CHECK_DIAMETER_DIFF_RATIO))
				temp.add(filteredContours.get(i));

		}
		this.filteredContours = temp;


	}

	public void postProcess() {
		ballList.addAll(filteredContours);

		Iterator<Ball> iter2 = ballList.iterator();
		while (iter2.hasNext()) {
			Ball ptr = iter2.next();
			//cvDrawContours(result, ptr.getBallContour(), CV_RGB(255, 0, 0), CV_RGB(0, 0, 0), -1, CV_FILLED, 8, cvPoint(0, 0));
			ballStat.add(new BallStat(cvContourArea(ptr.getBallContour(), CV_WHOLE_SEQ,0)));
		}
	}
	// TODO : sort balls in correct order
	public void sortBallsByOrder() {
		if(filteredContours.size() == 0) return;
		
		Ball firstBall = filteredContours.get(0);
		ClusteredBallList clustered = new ClusteredBallList(firstBall);
		ArrayList<ClusteredBallList> clusteredList = new ArrayList<ClusteredBallList>();
		clusteredList.add(clustered);
		for ( int i = 1; i < filteredContours.size(); i++ ) {
			Ball toInsert = filteredContours.get(i);
			for( int j = 0; j < clusteredList.size(); j++ ) {
				clustered = clusteredList.get(j);
				if (clustered.isInRangeY(toInsert)) {
					clustered.addBall(toInsert);
					break;
				}
				else if (clustered.compareY(toInsert) < 0) {
					clusteredList.add(j, new ClusteredBallList(toInsert));
					break;
				}
				else if (clustered.compareY(toInsert) >= 0 && j == clusteredList.size()-1) {
					clusteredList.ensureCapacity(1);
					clusteredList.add(j, new ClusteredBallList(toInsert));
					break;
				}
			}
		}
		
		ArrayList<Ball> sorted = new ArrayList<Ball>();
		for ( int i = 0; i < clusteredList.size(); i++ ) {
			clustered = clusteredList.get(i);
			ArrayList<Ball> balls = clustered.getBallList();
			for ( int j = 0; j < balls.size(); j++ ) {
				sorted.add(balls.get(j));
			}
		}
		this.filteredContours = sorted;
	}

}